summaryrefslogtreecommitdiff
path: root/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx')
-rw-r--r--app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx719
1 files changed, 719 insertions, 0 deletions
diff --git a/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx b/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx
new file mode 100644
index 00000000..79f1b147
--- /dev/null
+++ b/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx
@@ -0,0 +1,719 @@
+"use client";
+
+import { useState, useEffect, useCallback, useMemo } from "react";
+import { useParams } from "next/navigation";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { InfoIcon, RefreshCw, Search, Upload, Plus, Loader2 } from "lucide-react";
+import { toast } from "sonner";
+import { useTranslation } from "@/i18n/client";
+import {
+ UnifiedDwgReceiptItem,
+ DetailDwgReceiptItem,
+ FileInfoItem,
+ fetchDwgReceiptList,
+ getVendorSessionInfo,
+ fetchVendorProjects,
+ fetchDetailDwgReceiptList,
+ fetchFileInfoList,
+} from "@/lib/dolce/actions";
+import { DrawingListTableV2 } from "@/lib/dolce/table/drawing-list-table-v2";
+import { drawingListColumns } from "@/lib/dolce/table/drawing-list-columns";
+import { createGttDrawingListColumns, DocumentType } from "@/lib/dolce/table/gtt-drawing-list-columns";
+import { createDetailDrawingColumns } from "@/lib/dolce/table/detail-drawing-columns";
+import { createFileListColumns } from "@/lib/dolce/table/file-list-columns";
+// V2: MatchBatchFileDwg API를 사용하지 않는 새로운 일괄 업로드 (DetailDwgReceiptMgmtEdit 사용)
+import { B4BulkUploadDialogV2 } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog-v2";
+// V1로 되돌리려면: 위 줄을 주석 처리하고 아래 줄의 주석을 해제하세요
+// import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog";
+import { AddDetailDrawingDialog } from "@/lib/dolce/dialogs/add-detail-drawing-dialog";
+import { UploadFilesToDetailDialog } from "@/lib/dolce/dialogs/upload-files-to-detail-dialog";
+
+interface DolceUploadPageV2Props {
+ searchParams: { [key: string]: string | string[] | undefined };
+}
+
+export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Props) {
+ const params = useParams();
+ const lng = params?.lng as string;
+ const { t } = useTranslation(lng, "dolce");
+
+ // URL에서 초기 프로젝트 코드
+ const initialProjNo = (searchParams.projNo as string) || "";
+
+ // 상태 관리
+ const [drawings, setDrawings] = useState<UnifiedDwgReceiptItem[]>([]);
+ const [projects, setProjects] = useState<Array<{ code: string; name: string }>>([]);
+ const [vendorInfo, setVendorInfo] = useState<{
+ userId: string;
+ userName: string;
+ email: string;
+ vendorCode: string;
+ vendorName: string;
+ drawingKind: "B3" | "B4";
+ } | null>(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+ // 필터 상태
+ const [projNo, setProjNo] = useState(initialProjNo);
+ const [drawingNo, setDrawingNo] = useState("");
+ const [drawingName, setDrawingName] = useState("");
+ const [discipline, setDiscipline] = useState("");
+ const [manager, setManager] = useState("");
+ const [documentType, setDocumentType] = useState<DocumentType>("ALL"); // B4 전용
+
+ // 선택된 도면 및 상세도면
+ const [selectedDrawing, setSelectedDrawing] = useState<UnifiedDwgReceiptItem | null>(null);
+ const [detailDrawings, setDetailDrawings] = useState<DetailDwgReceiptItem[]>([]);
+ const [selectedDetail, setSelectedDetail] = useState<DetailDwgReceiptItem | null>(null);
+ const [files, setFiles] = useState<FileInfoItem[]>([]);
+ const [isLoadingDetails, setIsLoadingDetails] = useState(false);
+ const [isLoadingFiles, setIsLoadingFiles] = useState(false);
+
+ // 다이얼로그
+ const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = useState(false);
+ const [addDialogOpen, setAddDialogOpen] = useState(false);
+ const [uploadFilesDialogOpen, setUploadFilesDialogOpen] = useState(false);
+
+ // 초기 데이터 로드
+ const loadInitialData = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ // 병렬로 데이터 로드
+ const [vendorInfoData, projectsData] = await Promise.all([
+ getVendorSessionInfo(),
+ fetchVendorProjects(),
+ ]);
+
+ setVendorInfo(vendorInfoData as typeof vendorInfo);
+ setProjects(projectsData);
+
+ // 초기 프로젝트가 있으면 도면 로드
+ if (initialProjNo) {
+ const drawingsData = await fetchDwgReceiptList({
+ project: initialProjNo,
+ drawingKind: vendorInfoData.drawingKind,
+ drawingVendor: vendorInfoData.drawingKind === "B3" ? vendorInfoData.vendorCode : "",
+ });
+ setDrawings(drawingsData);
+ }
+ } catch (err) {
+ console.error("초기 데이터 로드 실패:", err);
+ setError(err instanceof Error ? err.message : t("page.initialLoadError"));
+ toast.error(t("page.initialLoadError"));
+ } finally {
+ setIsLoading(false);
+ }
+ }, [initialProjNo, t]);
+
+ // 도면 목록 조회
+ const loadDrawings = useCallback(async () => {
+ if (!projNo || !vendorInfo) return;
+
+ try {
+ setIsRefreshing(true);
+ setError(null);
+
+ const drawingsData = await fetchDwgReceiptList({
+ project: projNo,
+ drawingKind: vendorInfo.drawingKind,
+ drawingVendor: vendorInfo.drawingKind === "B3" ? vendorInfo.vendorCode : "",
+ });
+
+ setDrawings(drawingsData);
+ toast.success(t("page.drawingLoadSuccess"));
+ } catch (err) {
+ console.error("도면 로드 실패:", err);
+ setError(err instanceof Error ? err.message : t("page.drawingLoadError"));
+ toast.error(t("page.drawingLoadError"));
+ } finally {
+ setIsRefreshing(false);
+ }
+ }, [projNo, vendorInfo, t]);
+
+ // 상세도면 목록 로드
+ const loadDetailDrawings = useCallback(async () => {
+ if (!selectedDrawing) {
+ setDetailDrawings([]);
+ setSelectedDetail(null);
+ return;
+ }
+
+ try {
+ setIsLoadingDetails(true);
+ const data = await fetchDetailDwgReceiptList({
+ project: selectedDrawing.ProjectNo,
+ drawingNo: selectedDrawing.DrawingNo,
+ discipline: selectedDrawing.Discipline,
+ drawingKind: selectedDrawing.DrawingKind,
+ userId: "", // 조회 시 모든 사용자의 상세도면을 보기 위해 빈 문자열 전달
+ });
+ setDetailDrawings(data);
+
+ // 첫 번째 상세도면 자동 선택
+ if (data.length > 0) {
+ setSelectedDetail(data[0]);
+ } else {
+ setSelectedDetail(null);
+ }
+ } catch (error) {
+ console.error("상세도면 로드 실패:", error);
+ toast.error(t("detailDialog.detailLoadError"));
+ setDetailDrawings([]);
+ setSelectedDetail(null);
+ } finally {
+ setIsLoadingDetails(false);
+ }
+ }, [selectedDrawing, t]);
+
+ // 파일 목록 로드
+ const loadFiles = useCallback(async () => {
+ if (!selectedDetail) {
+ setFiles([]);
+ return;
+ }
+
+ try {
+ setIsLoadingFiles(true);
+ const data = await fetchFileInfoList(selectedDetail.UploadId);
+ setFiles(data);
+ } catch (error) {
+ console.error("파일 목록 로드 실패:", error);
+ toast.error(t("detailDialog.fileLoadError"));
+ setFiles([]);
+ } finally {
+ setIsLoadingFiles(false);
+ }
+ }, [selectedDetail, t]);
+
+ // 초기 데이터 로드
+ useEffect(() => {
+ loadInitialData();
+ }, [loadInitialData]);
+
+ // 프로젝트 변경 시 자동 검색
+ useEffect(() => {
+ if (projNo && vendorInfo) {
+ loadDrawings();
+ }
+ }, [projNo, vendorInfo, loadDrawings]);
+
+ // 선택된 도면 변경 시 상세도면 로드
+ useEffect(() => {
+ loadDetailDrawings();
+ }, [selectedDrawing, loadDetailDrawings]);
+
+ // 선택된 상세도면 변경 시 파일 목록 로드
+ useEffect(() => {
+ loadFiles();
+ }, [selectedDetail, loadFiles]);
+
+ // 도면 클릭 핸들러
+ const handleDrawingClick = (drawing: UnifiedDwgReceiptItem) => {
+ setSelectedDrawing(drawing);
+ };
+
+ // 검색 핸들러
+ const handleSearch = () => {
+ loadDrawings();
+ };
+
+ // 새로고침 핸들러
+ const handleRefresh = () => {
+ loadDrawings();
+ };
+
+ // 상세도면 새로고침 핸들러
+ const handleRefreshDetails = () => {
+ loadDetailDrawings();
+ };
+
+ // 일괄 업로드 완료 핸들러
+ const handleBulkUploadComplete = () => {
+ loadDrawings();
+ };
+
+ // 상세도면 추가 완료 핸들러
+ const handleAddComplete = () => {
+ setAddDialogOpen(false);
+ loadDetailDrawings();
+ };
+
+ // 파일 업로드 완료 핸들러
+ const handleUploadComplete = () => {
+ setUploadFilesDialogOpen(false);
+ loadFiles();
+ };
+
+ // 파일 다운로드 핸들러
+ const handleDownload = async (file: FileInfoItem) => {
+ try {
+ toast.info(t("detailDialog.downloadPreparing"));
+
+ // 파일 생성자의 userId를 사용하여 다운로드
+ const response = await fetch("/api/dolce/download", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ fileId: file.FileId,
+ userId: file.CreateUserId, // 파일 생성자의 ID 사용
+ fileName: file.FileName,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(t("detailDialog.downloadError"));
+ }
+
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = file.FileName;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+
+ toast.success(t("detailDialog.downloadSuccess"));
+ } catch (error) {
+ console.error("파일 다운로드 실패:", error);
+ toast.error(t("detailDialog.downloadError"));
+ }
+ };
+
+ // 필터된 도면 목록 (클라이언트 사이드 필터링)
+ const filteredDrawings = useMemo(() => {
+ let result = drawings.filter((drawing) => {
+ // 도면번호 필터 (공백 포함)
+ if (drawingNo && !drawing.DrawingNo.toLowerCase().includes(drawingNo.toLowerCase())) {
+ return false;
+ }
+
+ // 도면명 필터 (공백 포함)
+ if (drawingName && !drawing.DrawingName.toLowerCase().includes(drawingName.toLowerCase())) {
+ return false;
+ }
+
+ // 설계공종 필터 (공백 포함)
+ if (discipline && !drawing.Discipline?.toLowerCase().includes(discipline.toLowerCase())) {
+ return false;
+ }
+
+ // 담당자명 필터 (공백 포함)
+ if (manager && !drawing.Manager.toLowerCase().includes(manager.toLowerCase()) &&
+ !drawing.ManagerENM?.toLowerCase().includes(manager.toLowerCase())) {
+ return false;
+ }
+
+ return true;
+ });
+
+ // B4인 경우 Document Type 필터 적용
+ if (vendorInfo?.drawingKind === "B4" && documentType !== "ALL") {
+ result = result.filter((drawing) => {
+ // B4 타입 체크
+ if (drawing.DrawingKind !== "B4") return false;
+
+ // B4 도면의 DrawingMoveGbn 체크
+ const gttDrawing = drawing as { DrawingMoveGbn?: string };
+
+ if (documentType === "SHI_INPUT") {
+ return gttDrawing.DrawingMoveGbn === "도면제출";
+ } else if (documentType === "GTT_DELIVERABLES") {
+ return gttDrawing.DrawingMoveGbn === "도면입수";
+ }
+ return true;
+ });
+ }
+
+ return result;
+ }, [drawings, drawingNo, drawingName, discipline, manager, vendorInfo?.drawingKind, documentType]);
+
+ // RegisterId + UploadId 조합으로 고유 ID 생성
+ const getDetailDrawingId = (detail: DetailDwgReceiptItem) => {
+ return `${detail.RegisterId}_${detail.UploadId}`;
+ };
+
+ // 도면 고유 ID 생성
+ const getDrawingId = (drawing: UnifiedDwgReceiptItem) => {
+ return `${drawing.ProjectNo}_${drawing.DrawingNo}_${drawing.Discipline}`;
+ };
+
+ // B4인 경우 "도면입수"인 건만 상세도면 추가 및 파일 첨부 가능
+ // B3인 경우 모든 건에 대해 가능
+ const canAddDetailDrawing = vendorInfo && (
+ vendorInfo.drawingKind === "B3" ||
+ (vendorInfo.drawingKind === "B4" && selectedDrawing && 'DrawingMoveGbn' in selectedDrawing && selectedDrawing.DrawingMoveGbn === "도면입수")
+ );
+
+ const fileColumns = createFileListColumns({ onDownload: handleDownload, lng });
+
+ if (isLoading) {
+ return (
+ <Card>
+ <CardHeader>
+ <Skeleton className="h-8 w-48" />
+ <Skeleton className="h-4 w-96" />
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <Skeleton className="h-32 w-full" />
+ <Skeleton className="h-96 w-full" />
+ </CardContent>
+ </Card>
+ );
+ }
+
+ return (
+ <div className="space-y-4 max-w-full overflow-x-hidden h-full flex flex-col">
+ {/* 에러 메시지 */}
+ {error && (
+ <Alert variant="destructive">
+ <AlertDescription>{error}</AlertDescription>
+ </Alert>
+ )}
+
+ {/* 안내 메시지 */}
+ {!projNo && (
+ <Alert>
+ <InfoIcon className="h-4 w-4" />
+ <AlertDescription>
+ {t("page.selectProject")}
+ </AlertDescription>
+ </Alert>
+ )}
+
+ {/* 필터 카드 - 슬림하게 */}
+ <Card className="flex-shrink-0">
+ <CardHeader className="py-3">
+ <CardTitle className="text-base">{t("filter.title")}</CardTitle>
+ </CardHeader>
+ <CardContent className="py-3">
+ <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-3">
+ {/* 프로젝트 선택 */}
+ <div className="space-y-1">
+ <Label className="text-xs">{t("filter.project")}</Label>
+ <Select value={projNo} onValueChange={setProjNo}>
+ <SelectTrigger className="h-8">
+ <SelectValue placeholder={t("filter.projectPlaceholder")} />
+ </SelectTrigger>
+ <SelectContent>
+ {projects.map((project) => (
+ <SelectItem key={project.code} value={project.code}>
+ {project.code} - {project.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+
+ {/* 도면번호 검색 */}
+ <div className="space-y-1">
+ <Label className="text-xs">{t("filter.drawingNo")}</Label>
+ <Input
+ className="h-8"
+ value={drawingNo}
+ onChange={(e) => setDrawingNo(e.target.value)}
+ placeholder={t("filter.drawingNoPlaceholder")}
+ />
+ </div>
+
+ {/* 도면명 검색 */}
+ <div className="space-y-1">
+ <Label className="text-xs">{t("filter.drawingName")}</Label>
+ <Input
+ className="h-8"
+ value={drawingName}
+ onChange={(e) => setDrawingName(e.target.value)}
+ placeholder={t("filter.drawingNamePlaceholder")}
+ />
+ </div>
+
+ {/* 설계공종 검색 */}
+ <div className="space-y-1">
+ <Label className="text-xs">{t("filter.discipline")}</Label>
+ <Input
+ className="h-8"
+ value={discipline}
+ onChange={(e) => setDiscipline(e.target.value)}
+ placeholder={t("filter.disciplinePlaceholder")}
+ />
+ </div>
+
+ {/* 담당자명 검색 */}
+ <div className="space-y-1">
+ <Label className="text-xs">{t("filter.manager")}</Label>
+ <Input
+ className="h-8"
+ value={manager}
+ onChange={(e) => setManager(e.target.value)}
+ placeholder={t("filter.managerPlaceholder")}
+ />
+ </div>
+
+ {/* B4(GTT) 전용: Document Type 필터 */}
+ {vendorInfo?.drawingKind === "B4" && (
+ <div className="space-y-1">
+ <Label className="text-xs">{t("filter.documentType")}</Label>
+ <Select value={documentType} onValueChange={(value) => setDocumentType(value as DocumentType)}>
+ <SelectTrigger className="h-8">
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="ALL">{t("filter.documentTypeAll")}</SelectItem>
+ <SelectItem value="GTT_DELIVERABLES">{t("filter.documentTypeGttDeliverables")}</SelectItem>
+ <SelectItem value="SHI_INPUT">{t("filter.documentTypeSHIInput")}</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ )}
+ </div>
+
+ <div className="flex gap-2 mt-3 justify-end">
+ <Button
+ size="sm"
+ onClick={handleSearch}
+ disabled={!projNo || isRefreshing}
+ >
+ <Search className="h-4 w-4 mr-2" />
+ {t("filter.searchButton")}
+ </Button>
+ {/* B4 벤더인 경우에만 일괄 업로드 버튼 표시 */}
+ {vendorInfo?.drawingKind === "B4" && (
+ <Button
+ size="sm"
+ variant="default"
+ onClick={() => setBulkUploadDialogOpen(true)}
+ disabled={!projNo || isRefreshing}
+ >
+ <Upload className="h-4 w-4 mr-2" />
+ {t("filter.bulkUploadButton")}
+ </Button>
+ )}
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={handleRefresh}
+ disabled={!projNo || isRefreshing}
+ >
+ <RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
+ </Button>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 도면 리스트 테이블 - 항상 렌더링 */}
+ <Card className="flex-shrink-0" style={{ minHeight: "500px" }}>
+ <CardHeader className="py-3">
+ <CardTitle className="text-base">
+ {t("drawingList.title")}
+ {filteredDrawings.length > 0 && ` ${t("drawingList.count", { count: filteredDrawings.length })}`}
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="p-0">
+ {!projNo || !vendorInfo ? (
+ <div className="flex items-center justify-center text-muted-foreground p-8" style={{ minHeight: "400px" }}>
+ <div className="text-center">
+ <InfoIcon className="h-12 w-12 mx-auto mb-2 opacity-50" />
+ <p>{t("page.selectProject")}</p>
+ </div>
+ </div>
+ ) : isRefreshing ? (
+ <div className="flex items-center justify-center" style={{ minHeight: "400px" }}>
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
+ </div>
+ ) : (
+ <DrawingListTableV2
+ columns={
+ vendorInfo.drawingKind === "B4"
+ ? (createGttDrawingListColumns({ documentType, lng, t }) as unknown as typeof drawingListColumns)
+ : (drawingListColumns(lng, t) as unknown as typeof drawingListColumns)
+ }
+ data={filteredDrawings}
+ onRowClick={handleDrawingClick}
+ selectedRow={selectedDrawing || undefined}
+ getRowId={getDrawingId}
+ maxHeight="calc(100vh - 600px)"
+ minHeight="400px"
+ defaultPageSize={10}
+ />
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 하단: 상세도면리스트 + 파일리스트 - 항상 렌더링 */}
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 flex-1 min-h-0" style={{ minHeight: "500px" }}>
+ {/* 좌측: 상세도면 리스트 */}
+ <Card className="flex flex-col min-h-0">
+ <CardHeader className="flex-row items-center justify-between py-3 flex-shrink-0">
+ <CardTitle className="text-base">
+ {t("detailDialog.detailListTitle")}
+ {selectedDrawing && (
+ <span className="text-xs font-normal text-muted-foreground ml-2">
+ {selectedDrawing.DrawingNo}
+ </span>
+ )}
+ </CardTitle>
+ <div className="flex gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleRefreshDetails}
+ disabled={!selectedDrawing || isLoadingDetails}
+ >
+ <RefreshCw className={`h-4 w-4 ${isLoadingDetails ? "animate-spin" : ""}`} />
+ </Button>
+ {canAddDetailDrawing && (
+ <Button
+ variant="default"
+ size="sm"
+ onClick={() => setAddDialogOpen(true)}
+ disabled={!selectedDrawing}
+ >
+ <Plus className="h-4 w-4 mr-2" />
+ {t("detailDialog.addDetailButton")}
+ </Button>
+ )}
+ </div>
+ </CardHeader>
+ <CardContent className="flex-1 p-0 min-h-0">
+ {!selectedDrawing ? (
+ <div className="h-full flex items-center justify-center text-muted-foreground p-4" style={{ minHeight: "400px" }}>
+ <div className="text-center">
+ <InfoIcon className="h-12 w-12 mx-auto mb-2 opacity-50" />
+ <p>도면을 선택해주세요</p>
+ </div>
+ </div>
+ ) : isLoadingDetails ? (
+ <div className="flex items-center justify-center h-full" style={{ minHeight: "400px" }}>
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
+ </div>
+ ) : (
+ <DrawingListTableV2<DetailDwgReceiptItem, unknown>
+ columns={createDetailDrawingColumns(lng, t)}
+ data={detailDrawings}
+ onRowClick={setSelectedDetail}
+ selectedRow={selectedDetail || undefined}
+ getRowId={getDetailDrawingId}
+ maxHeight="calc(100vh - 600px)"
+ minHeight="400px"
+ defaultPageSize={10}
+ />
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 우측: 첨부파일 리스트 */}
+ <Card className="flex flex-col min-h-0">
+ <CardHeader className="flex-row items-center justify-between py-3 flex-shrink-0">
+ <CardTitle className="text-base">
+ {t("detailDialog.fileListTitle")}
+ {selectedDetail && (
+ <span className="text-xs font-normal text-muted-foreground ml-2">
+ Rev. {selectedDetail.DrawingRevNo}
+ </span>
+ )}
+ </CardTitle>
+ {selectedDetail && canAddDetailDrawing && (
+ <Button
+ variant="default"
+ size="sm"
+ onClick={() => setUploadFilesDialogOpen(true)}
+ >
+ <Upload className="h-4 w-4 mr-2" />
+ {t("detailDialog.uploadFilesButton")}
+ </Button>
+ )}
+ </CardHeader>
+ <CardContent className="flex-1 p-0 min-h-0">
+ {!selectedDetail ? (
+ <div className="h-full flex items-center justify-center text-muted-foreground p-4" style={{ minHeight: "400px" }}>
+ <div className="text-center">
+ <InfoIcon className="h-12 w-12 mx-auto mb-2 opacity-50" />
+ <p>{t("detailDialog.selectDetailDrawing")}</p>
+ </div>
+ </div>
+ ) : isLoadingFiles ? (
+ <div className="flex items-center justify-center h-full" style={{ minHeight: "400px" }}>
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
+ </div>
+ ) : (
+ <DrawingListTableV2
+ columns={fileColumns}
+ data={files}
+ maxHeight="calc(100vh - 600px)"
+ minHeight="400px"
+ defaultPageSize={10}
+ />
+ )}
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* B4 일괄 업로드 다이얼로그 (V2) */}
+ {/* V2: MatchBatchFileDwg API를 사용하지 않는 새로운 방식 */}
+ {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && (
+ <B4BulkUploadDialogV2
+ open={bulkUploadDialogOpen}
+ onOpenChange={setBulkUploadDialogOpen}
+ projectNo={projNo}
+ userId={vendorInfo.userId}
+ userName={vendorInfo.userName}
+ userEmail={vendorInfo.email}
+ vendorCode={vendorInfo.vendorCode}
+ onUploadComplete={handleBulkUploadComplete}
+ lng={lng}
+ />
+ )}
+ {/* V1로 되돌리려면: 위의 B4BulkUploadDialogV2를 B4BulkUploadDialog로 변경하세요 */}
+
+ {/* 상세도면 추가 다이얼로그 */}
+ {vendorInfo && selectedDrawing && (
+ <AddDetailDrawingDialog
+ open={addDialogOpen}
+ onOpenChange={setAddDialogOpen}
+ drawing={selectedDrawing}
+ vendorCode={vendorInfo.vendorCode}
+ userId={vendorInfo.userId}
+ userName={vendorInfo.userName}
+ userEmail={vendorInfo.email}
+ onComplete={handleAddComplete}
+ drawingKind={vendorInfo.drawingKind}
+ lng={lng}
+ />
+ )}
+
+ {/* 파일 업로드 다이얼로그 */}
+ {vendorInfo && selectedDetail && (
+ <UploadFilesToDetailDialog
+ open={uploadFilesDialogOpen}
+ onOpenChange={setUploadFilesDialogOpen}
+ uploadId={selectedDetail.UploadId}
+ drawingNo={selectedDetail.DrawingNo}
+ revNo={selectedDetail.DrawingRevNo}
+ userId={vendorInfo.userId}
+ onUploadComplete={handleUploadComplete}
+ lng={lng}
+ />
+ )}
+ </div>
+ );
+}
+